using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
//using SecretLabs.NETMF.Hardware;
using System.IO.Ports;
using System.Text;
using System.Collections;

#if MINI
using SecretLabs.NETMF.Hardware.NetduinoMini;
#else
using SecretLabs.NETMF.Hardware.Netduino;
#endif

namespace codebase
{
    public class MIDIFileParser
    {
        private byte[] fileBuffer;

        public uint index;
        public uint trackLength;
        public short TicksPerBeat { get; private set; }
        public int Tempo { get; private set; }
        public NoteEvent note;

        public struct NoteEvent
        {
            public uint DeltaTime;
            public byte EventType;
            public byte NoteNumber;
        }

        static PWM piezo;
        public MIDIFileParser(PWM _piezo)
        {
            note = new NoteEvent();
            piezo = _piezo;
        }

        public static void PlaySong(byte[] file, PWM _piezo)
        {
            // Create the MIDI parser
            MIDIFileParser parser = new MIDIFileParser(_piezo);

            // Prepare the file for parsing
            parser.SetFile(file);

            // Parse until we reach the end of the track
            while (parser.index < parser.trackLength)
            {
                parser.ReadTrackEvent(); // Parse the track event
                if (parser.note.EventType.Equals(0)) continue;

                // Get the note event from parsing
                MIDIFileParser.NoteEvent noteEvent = (MIDIFileParser.NoteEvent)parser.note;

                // Determine the time to wait
                TimeSpan timeToWait = new TimeSpan(parser.note.DeltaTime * parser.Tempo * TimeSpan.TicksPerMillisecond / 1000 / parser.TicksPerBeat);
                Debug.Print("Time To Wait: " + timeToWait.ToString());
                Thread.Sleep((int)(timeToWait.Ticks / TimeSpan.TicksPerMillisecond));

                // Determine what to do with this note event
                if ((parser.note.EventType & 0xF0) == 0x80)
                {
                    // Note Off
                    Debug.Print("NOTE OFF: " + parser.note.NoteNumber.ToString());
                    //piezo.Set(0, 0);
                    piezo.Stop();
                }
                else if ((parser.note.EventType & 0xF0) == 0x90)
                {
                    // Note On
                    Debug.Print("NOTE ON: " + parser.note.NoteNumber.ToString());
                    PlayNote(parser.note.NoteNumber);
                }
            }

            //piezo.Set(0, 0);
            piezo.Stop();
            Debug.Print("No more notes!");
        }

        public static void PlayNote(int noteNum)
        {
            int freq = GetFreq(noteNum);
            Debug.Print("Freq: " + freq.ToString());
            //piezo.Set(freq, 50);
            piezo.Frequency = freq;
            piezo.Start();
        }

        public static int GetFreq(int noteNum)
        {
            int diff = noteNum - 69;
            return (int)(440 * System.Math.Pow(2, (double)diff / 12));
        }

        public void SetFile(byte[] file)
        {
            this.Tempo = 500000;

            fileBuffer = file;

            ReadHeaderChunk();
            BurnTrackChunk();
            ReadTrackChunk(this.index);
        }

        private bool ReadHeaderChunk()
        {
            // Read "MThd"
            if (GetNextByte() == 0x4D &&
            GetNextByte() == 0x54 &&
            GetNextByte() == 0x68 &&
            GetNextByte() == 0x64)
            {
                // Read header length (should always be 6)
                if (GetNextByte() == 0x00 &&
                GetNextByte() == 0x00 &&
                GetNextByte() == 0x00 &&
                GetNextByte() == 0x06)
                {
                    // Read format - 0 = single track, 1 = multiple track, 2 = multiple song
                    GetNextByte();
                    GetNextByte();

                    // Read number of tracks that follow the header chunk
                    GetNextByte();
                    GetNextByte();

                    // Read the division (ticks per beat)
                    this.TicksPerBeat = (short)((short)GetNextByte() << 8);
                    this.TicksPerBeat += GetNextByte();

                    return true;
                }
            }

            return false;
        }

        private void BurnTrackChunk()
        {
            // Read "MTrk"
            if (GetNextByte() == 0x4d &&
            GetNextByte() == 0x54 &&
            GetNextByte() == 0x72 &&
            GetNextByte() == 0x6B)
            {
                // Read track length
                this.trackLength = (uint)((uint)GetNextByte() << 24);
                this.trackLength += (uint)((uint)GetNextByte() << 16);
                this.trackLength += (uint)((uint)GetNextByte() << 8);
                this.trackLength += GetNextByte();

                this.index += this.trackLength;
            }
        }

        private bool ReadTrackChunk(uint startIndex)
        {
            // Read "MTrk"
            if (GetNextByte() == 0x4d &&
            GetNextByte() == 0x54 &&
            GetNextByte() == 0x72 &&
            GetNextByte() == 0x6B)
            {
                // Read track length
                this.trackLength = (uint)((uint)GetNextByte() << 24);
                this.trackLength += (uint)((uint)GetNextByte() << 16);
                this.trackLength += (uint)((uint)GetNextByte() << 8);
                this.trackLength += GetNextByte();

                return true;
            }

            return false;
        }

        public void ReadTrackEvent()
        {
            uint deltaTime = ReadDeltaTime();

            byte eventType = GetNextByte();

            if (eventType == 0xF0 ||
            eventType == 0xF7)
            {
                Debug.Print("Sys Ex Event");
                ReadSysExEvent();
            }
            else if (eventType == 0xFF)
            {
                Debug.Print("Meta Event");
                ReadMetaEvent();
            }
            else
            {
                Debug.Print("Midi Event");
                ReadMidiEvent(eventType, deltaTime);
            }
        }

        private byte runningStatus;
        private void ReadMidiEvent(byte eventTypeIn, uint deltaTime)
        {
            byte eventType;
            byte parm1, parm2;

            // Check for running status
            if ((eventTypeIn & 0x80) > 0)
            {
                eventType = eventTypeIn;
                runningStatus = eventType;
                parm1 = GetNextByte();
            }
            else
            {
                // running status, use last value
                eventType = runningStatus;
                parm1 = eventTypeIn;
            }

            this.note.DeltaTime = deltaTime;
            this.note.EventType = eventType;
            this.note.NoteNumber = parm1;

            if ((eventType & 0xF0) == 0x80)
            {
                // Note Off
                Debug.Print("Note Off: " + parm1.ToString());
                parm2 = GetNextByte();
            }
            else if ((eventType & 0xF0) == 0x90)
            {
                // Note On
                Debug.Print("Note On: " + parm1.ToString());
                parm2 = GetNextByte();
            }
            else
            {
                // Set the EventType to zero so we can ignore this note
                this.note.EventType = 0;

                if (((eventType & 0xF0) == 0xC0) ||
                ((eventType & 0xF0) == 0xD0))
                {

                }
                else
                {
                    parm2 = GetNextByte();
                }
                // Unhandled MIDI events
                Debug.Print("Unhandled MIDI Event: " + eventType.ToString());
            }
        }

        private void ReadMetaEvent()
        {
            byte metaType = GetNextByte();
            uint len = ReadVLength();

            if (metaType == 0x51)
            {
                this.Tempo = (int)((int)GetNextByte() << 16);
                this.Tempo += (int)((int)GetNextByte() << 8);
                this.Tempo += GetNextByte();
                Debug.Print("New Tempo: " + this.Tempo.ToString());
            }
            else
            {
                Debug.Print("Ignored Meta Event: Length=" + len.ToString());

                if (metaType == 0x2f)
                {
                    Debug.Print("End of Track");
                }

                this.index += len;
            }
        }

        private void ReadSysExEvent()
        {
            uint len = ReadVLength();

            for (int i = 0; i < len; i++)
            {
                GetNextByte();
            }
        }

        private uint ReadDeltaTime()
        {
            uint time = ReadVLength();
            Debug.Print("Delta Time: " + time.ToString());
            return time;
        }

        private uint ReadVLength()
        {
            uint length = 0;
            byte data;

            for (int i = 0; i < 4; i++)
            {
                data = GetNextByte();
                length += (uint)(data & 0x7F);
                if ((data & 0x80) == 0)
                {
                    break;
                }
                else
                {
                    length <<= 7;
                }
            }

            return length;
        }

        private byte GetNextByte()
        {
            if (index < this.fileBuffer.Length)
            {
                //Debug.Print("Byte: " + this.fileBuffer[index].ToString());
                return this.fileBuffer[index++];
            }
            else
            {
                return 0x00;
            }
        }
    }
}
